/*______________________________________________________________________________
 *
 * Copyright 2005 NORSYS
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * (1) Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 * (2) Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 * (3) The name of the author may not be used to endorse or promote
 *     products derived from this software without specific prior
 *     written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *______________________________________________________________________________
 *
 * Created on 27 sept. 2005
 * Author: Arnaud Bailly
 */
package speculoos.jndi.mappers;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.naming.Name;
import javax.naming.NameParser;
import javax.naming.NamingException;

import org.apache.commons.collections.BidiMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import speculoos.core.ChainMapper;
import speculoos.core.Mapper;
import speculoos.core.MapperCollector;
import speculoos.core.MapperException;
import speculoos.jndi.JNDIConnection;
import speculoos.spi.MapperTechnicalException;
import speculoos.utils.TypeHelper;
import speculoos.utils.VariableString;
import speculoos.valid.MapMandatoryValidator;

/**
 * Base implementation for mappings in the context of a JNDI data source.
 * <p>
 * This is the base implementation class for all JNDI/LDAP mappers. It contains
 * common properties:
 * <dl>
 * <dt>directory</dt>
 * <dd>The reference directory context this mapper operates on. This is set by
 * the source attached to this mapper. This is a
 * {@link javax.naming.directory.DirContext} instance.</dd>
 * <dt>root</dt>
 * <dd>The node this mapper operates on or from. This is a
 * {@link speculoos.utils.VariableString} string that must be later on
 * instanciated by subclasses to yield a string.</dd>
 * <dt>environment</dt>
 * <dd>The base environment as set by the source upon creation. This must not
 * be modified as environment may be shared across multiple mappers.</dd>
 * <dt>typeHelper</dt>
 * <dd>Eases reflexive manipulation of typed data.</dd>
 * <dt>nameParser</dt>
 * <dd>Extracted from the directory, this is a {@link javax.naming.NameParser}
 * instance that may be used by subclasses.</dd>
 * </dl>
 * Theses properties <strong>must never</strong> be modified by subclasses.
 * </p>
 * <p>
 * Subclasses <strong>must</strong> implement the {@link #clone()} method as it
 * is used by the source to instantiate a mapper with immediate sharing of
 * resources. This follows the <em>Prototype</em> design pattern as proposed
 * in the GOF.
 * </p>
 * 
 * @author nono
 * @version $Id: JNDIMapper.java 61 2005-10-21 14:54:44Z
 *          /C=FR/ST=Nord/L=Lille/O=Norsys SA/OU=UE/CN=Arnaud
 *          Bailly/emailAddress=abailly@norsys.fr $
 * @see javax.naming.directory.DirContext
 */
public abstract class JNDIMapper implements Mapper, Cloneable, MapperCollector {

	/*
	 * introspection
	 */
	protected TypeHelper typeHelper = new TypeHelper();

	/*
	 * the root context for thie mapper's operation
	 */
	protected JNDIConnection directory;

	/*
	 * the root name to operate on
	 */
	protected VariableString root;

	/* the directory name */
	private String directoryName;

	/* the parser used for transforming strings to names */
	protected NameParser nameParser;

	/*
	 * The environment as initialized when mapper is created
	 */
	protected Map environment = new HashMap();

	/*
	 * the name of this mapper
	 */
	private String name;

	/*
	 * output (translation) map
	 */
	private BidiMap output;

	/*
	 * One logger for all JNDI mappers
	 */
	protected static final Log log = LogFactory.getLog(JNDIMapper.class);

	/*
	 * chains for filtering input and output results
	 */
	private ChainMapper inputChain = new ChainMapper();

	private ChainMapper outputChain = new ChainMapper();

	protected Name _root;

	/**
	 * Create a mapper with given name.
	 * 
	 * @param value
	 *            name of mapper to create. May not be null or empty.
	 */
	public JNDIMapper(String value) {
		if (value == null || "".equals(value))
			throw new IllegalArgumentException(
					"Name of mapper may not be null or empty");
		this.name = value;
	}

	/**
	 * Default constructor for anonymous mapper.
	 * 
	 */
	public JNDIMapper() {
		this("");
	}

	/**
	 * @return Returns the environment.
	 */
	public Map getEnvironment() {
		return environment;
	}

	/**
	 * @param environment
	 *            The environment to set.
	 */
	public void setEnvironment(Map environment) {
		if (environment == null)
			throw new IllegalArgumentException(
					"May not set with a null argument");
		this.environment = environment;
	}

	/**
	 * @return Returns the directory.
	 */
	public JNDIConnection getDirectory() {
		return directory;
	}

	/**
	 * @param directory
	 *            The directory to set.
	 * @throws NamingException
	 */
	public void setDirectory(JNDIConnection directory) throws NamingException {
		this.directory = directory;
		if (directory == null)
			return;
		this.directoryName = directory.getNameInNamespace();
		this.nameParser = directory.getNameParser(directoryName);
	}

	/**
	 * @return Returns the root.
	 */
	public VariableString getRoot() {
		return root;
	}

	/**
	 * @param root
	 *            The root to set.
	 */
	public void setRoot(VariableString root) {
		this.root = root;
	}

	/**
	 * @return Returns the typeHelper.
	 */
	public TypeHelper getTypeHelper() {
		return typeHelper;
	}

	/**
	 * @param typeHelper
	 *            The typeHelper to set.
	 */
	public void setTypeHelper(TypeHelper typeHelper) {
		this.typeHelper = typeHelper;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#clone()
	 */
	public Object clone() {
		JNDIMapper src;
		try {
			src = (JNDIMapper) super.clone();
		} catch (CloneNotSupportedException e) {
			// SHOULD NOT HAPPEN
			throw new Error("Clone failed in JNDIMapper class");
		}
		src.name = name;
		/* reset directory (ie. connection) */
		src.directory = null;
		/* duplicate env */
		src.environment = new HashMap(environment);
		return src;
	}

	/**
	 * Returns the name of this mapper.
	 * 
	 * @return name of mapper.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Defines the input map to use.
	 * <p>
	 * The input map gives allowed properties that are to be used in a query.
	 * The map may contain default values which are used if not supplied in
	 * invocation.
	 * </p>
	 * <p>
	 * <strong>WARNING</strong>: currently handles only string attributes.
	 * </p>
	 * 
	 * @param map
	 *            a map from String to String
	 * @deprecated The definition of <code>input</code> attribute in a mapper
	 *             is kept for configuration compatibilty reasons. The preferred
	 *             way is to create validator instances of
	 *             {@link speculoos.jndi.valid.MapMandatoryValidator}.
	 * 
	 */
	public void setInput(Map map) {
		/* create mandatory validator */
		inputChain.addMapper(new MapMandatoryValidator(map));
	}

	/**
	 * sets the input chain to use.
	 * 
	 * @param in
	 *            a chain mapper to execute on input.
	 */
	public void setInputChain(ChainMapper in) {
		this.inputChain = in;
	}

	/**
	 * sets the output chain to use.
	 * 
	 * @param out
	 *            a ChainMapper to execute on output.
	 */
	public void setOutputChain(ChainMapper out) {
		this.outputChain = out;
	}

	/**
	 * return the input map used.
	 * 
	 * @return
	 * @deprecated
	 */
	public Map getInput() {
		throw new RuntimeException("This operation has been deprecated");
	}

	/**
	 * @param output
	 *            The output to set.
	 */
	public void setOutput(BidiMap output) {
		this.output = output;
	}

	/**
	 * 
	 * @return current output map.
	 */
	public BidiMap getOutput() {
		return output;
	}

	/**
	 * Release reference to the dircontext instance held.
	 * 
	 */
	public void clearDirectory() {
		this.directory = null;
	}

	/**
	 * @return Returns the inputChain.
	 */
	public ChainMapper getInputChain() {
		return inputChain;
	}

	/**
	 * @return Returns the outputChain.
	 */
	public ChainMapper getOutputChain() {
		return outputChain;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.Mapper#map(java.lang.Object, java.util.Map)
	 */
	public Object map(Object input, Map param) throws MapperException {
		/* update environment */
		param = updateEnv(param);
		param.put("self", input);
		updateRoot(param);
		/* filter input */
		input = inputChain.map(input, param);
		/* work */
		Object out = doMap(input, param);
		/* filter output */
		return outputChain.map(out, param);
	}

	private Map updateEnv(Map env) throws MapperTechnicalException {
		Map vars = new HashMap(environment);
		if (env != null)
			vars.putAll(env);
		return vars;
	}

	/**
	 * Update the root context from which this mapper operates.
	 * This method may be overriden to allow composite mappers the 
	 * opportunity to  make something else with root.
	 * 
	 * @param vars
	 * @throws MapperTechnicalException
	 */
	protected void updateRoot(Map vars) throws MapperTechnicalException {
		String rs = null;
		/* instanciate root */
		if (root == null) {
			// try looking in env
			rs = (String) vars.get("__ROOT__");
		} else
			rs = root.instance(vars);
		try {
			_root = nameParser.parse(rs);
			vars.put("root", _root);
			vars.put("__ROOT__", rs);
		} catch (NamingException e1) {
			throw new MapperTechnicalException(
					"Cannot resolve name for root context " + root);
		}
	}

	/**
	 * Main method to implement for this mappers subclasses.
	 * 
	 * @param input2
	 * @param param
	 * @return
	 * @throws MapperException
	 */
	protected abstract Object doMap(Object input2, Map param)
			throws MapperException;

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.MapperCollector#addMapper(speculoos.core.Mapper)
	 */
	public void addMapper(Mapper m) {
		inputChain.addMapper(m);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.MapperCollector#addMappers(java.util.Collection)
	 */
	public void addMappers(Collection col) {
		inputChain.addMappers(col);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.MapperCollector#getMappers()
	 */
	public Collection getMappers() {
		return inputChain.getMappers();
	}

}
